package redis

import (
	"context"
	"encoding/json"
	"github.com/bsm/redislock"
	"github.com/go-redis/redis/v8"
	"log"
	"tapay-bot/global"
	"tapay-bot/pkg/errcode"
	"tapay-bot/pkg/setting"
	"time"
)

var (
	client                *redis.Client = nil
	KeyPrefix                           = "distribution_key_"
	LockKeyRenewScript                  = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end"
	LockKeyRenewScriptSha               = "lock_key_renew_script_sha"
)

func GetClient() *redis.Client {
	return client
}

func getRedisOptions(redisSettings *setting.RedisSettings) *redis.Options {
	options := redis.Options{
		Addr: redisSettings.Addr,
		//Password:     redisSettings.Password,
		DB:           0,
		DialTimeout:  10 * time.Second,
		ReadTimeout:  30 * time.Second,
		WriteTimeout: 30 * time.Second,
		PoolSize:     10,
		MinIdleConns: 10,
		PoolTimeout:  30 * time.Second,
		IdleTimeout:  120 * time.Second,
	}
	if redisSettings.Username != "" {
		options.Username = redisSettings.Username
	}
	if redisSettings.Password != "" {
		options.Password = redisSettings.Password
	}
	return &options
}

func InitClient() error {
	client = redis.NewClient(getRedisOptions(global.RedisSetting))
	ctx := context.Background()
	_, err := client.Ping(ctx).Result()
	if err == redis.Nil {
		return err
	}
	if err != nil {
		return err
	}
	log.Println("=======================redis connection success !!!=======================")
	return nil
}

//func InitDevClient() error {
//	devClient = redis.NewClient(getRedisOptions(global.DevRedisSetting))
//	ctx := context.Background()
//	_, err := devClient.Ping(ctx).Result()
//	if err == redis.Nil {
//		return err
//	}
//	if err != nil {
//		return err
//	}
//	log.Println("=======================dev redis connection success !!!=======================")
//	return nil
//}

func TryLock(ctx context.Context, key string, ttl time.Duration) (*redislock.Lock, bool) {
	locker := redislock.New(client)
	// Retry every 100ms, for up-to 3x
	backoff := redislock.LimitRetry(redislock.LinearBackoff(100*time.Millisecond), 3)
	// Obtain lock with retry
	lock, err := locker.Obtain(ctx, KeyPrefix+key, ttl, &redislock.Options{
		RetryStrategy: backoff,
	})
	if err == redislock.ErrNotObtained {
		return lock, false
	} else if err != nil {
		log.Printf("Could not obtain lock,err: %v", err)
		return lock, false
	}
	return lock, true
}

func TryLockNoError(key string, ttl time.Duration, fc func()) {
	ctx := context.Background()
	lock, ok := tryLockByRetryCnt(ctx, key, ttl, 3)
	if !ok {
		return
	}
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	fc()
}

func TryLockThrowError(key string, ttl time.Duration, fc func() error) error {
	ctx := context.Background()
	lock, ok := tryLockByRetryCnt(ctx, key, ttl, 3)
	if !ok {
		return nil
	}
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	return fc()
}

func TryLockThrowError2(key string, ttl time.Duration, result interface{}, fc func() error) error {
	ctx := context.Background()
	cacheResult := Get(ctx, key)
	if cacheResult != "" {
		return json.Unmarshal([]byte(cacheResult), result)
	}
	var lock *redislock.Lock
	var ok bool
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	for i := 0; i < 3; i++ {
		lock, ok = TryLockOnce(ctx, key, ttl)
		if ok {
			break
		}
		time.Sleep(100 * time.Millisecond)
		cacheResult := Get(ctx, key)
		if cacheResult != "" {
			return json.Unmarshal([]byte(cacheResult), result)
		}
	}
	return fc()
}

// RepeatSubmitLock
//
//	@Author achilles
//	@Description: 防止重复提交
//	@param key
//	@param ttl
//	@param fc
//	@date 2022-03-27 10:34:06
//	@return error
func RepeatSubmitLock(key string, ttl time.Duration, fc func() error) error {
	ctx := context.Background()
	lock, ok := TryLockOnce(ctx, key, ttl)
	if !ok {
		return errcode.RepeatSubmitError
	}
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	return fc()
}

// RepeatSubmitLockNoError
//
//	@Author achilles
//	@Description: 防止重复提交不报错版
//	@param key
//	@param ttl
//	@param fc
//	@date 2022-03-27 10:34:26
func RepeatSubmitLockNoError(key string, ttl time.Duration, fc func()) {
	ctx := context.Background()
	lock, ok := TryLockOnce(ctx, key, ttl)
	if !ok {
		return
	}
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	fc()
}

func TryLockOnce(ctx context.Context, key string, ttl time.Duration) (*redislock.Lock, bool) {
	return tryLockByRetryCnt(ctx, key, ttl, 0)
}

func GetLockValue(key string) string {
	ctx := context.Background()
	realKey := KeyPrefix + key
	return Get(ctx, realKey)
}

func RepeatSubmitLockNoErrorWithWatch(key string, ttl time.Duration, fc func()) {
	ctx := context.Background()
	lock, ok := TryLockOnce(ctx, key, ttl)
	if !ok {
		return
	}
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	lockVal := GetLockValue(key)
	millis := ttl.Milliseconds() / int64(3)
	watchDog := NewWatchDog(key, lockVal, time.Duration(millis)*time.Millisecond, ttl)
	defer func() {
		watchDog <- "done"
		close(watchDog)
	}()
	fc()
}

func RepeatSubmitLockWithWatch(key string, ttl time.Duration, fc func() error) error {
	ctx := context.Background()
	lock, ok := TryLockOnce(ctx, key, ttl)
	if !ok {
		return nil
	}
	defer func() {
		if ok {
			lock.Release(ctx)
		}
	}()
	lockVal := GetLockValue(key)
	millis := ttl.Milliseconds() / int64(3)
	watchDog := NewWatchDog(key, lockVal, time.Duration(millis)*time.Millisecond, ttl)
	defer func() {
		watchDog <- "done"
		close(watchDog)
	}()
	return fc()
}

func getScriptSha(ctx context.Context, shaKey string, script string) string {
	sha := Get(ctx, shaKey)
	if len(sha) > 0 {
		return sha
	}
	result, err := client.ScriptLoad(ctx, script).Result()
	if err != nil {
		global.Logger.Errorf(ctx, "redis.getScriptSha err:%v", err)
		return ""
	}
	client.Set(ctx, shaKey, result, 24*time.Hour)
	return result
}

func getLockKeyRenewScript(ctx context.Context) string {
	return getScriptSha(ctx, LockKeyRenewScriptSha, LockKeyRenewScript)
}

func NewWatchDog(key string, value string, duration time.Duration, ttl time.Duration) chan string {
	realKey := KeyPrefix + key
	ctx := context.Background()
	c := make(chan string)
	tick := time.Tick(duration)
	go func() {
		for {
			select {
			case <-c:
				//fmt.Printf("====================NewWatchDog DONE=============================%s\n", key)
				return
			case <-tick:
				_, err := client.EvalSha(ctx, getLockKeyRenewScript(ctx), []string{realKey}, value, ttl.Milliseconds()).Result()
				if err != nil {
					global.Logger.Errorf(ctx, "redis.evalSha LockKeyRenewScript err:%v", err)
					return
				}
				//fmt.Printf("====================NewWatchDog result:%d\n", result)
			}
		}
	}()
	return c
}

func tryLockByRetryCnt(ctx context.Context, key string, ttl time.Duration, retryCnt int) (*redislock.Lock, bool) {
	locker := redislock.New(client)
	// Retry every 100ms, for up-to 3x
	backoff := redislock.LimitRetry(redislock.LinearBackoff(100*time.Millisecond), retryCnt)
	// Obtain lock with retry
	lock, err := locker.Obtain(ctx, KeyPrefix+key, ttl, &redislock.Options{
		RetryStrategy: backoff,
	})
	var ok = true
	if err == redislock.ErrNotObtained {
		ok = false
	} else if err != nil {
		log.Printf("Could not obtain lock,err: %v", err)
		ok = false
	}
	return lock, ok
}

func IncrBy(ctx context.Context, key string, incr int64, expiration time.Duration) int64 {
	result, err := client.IncrBy(ctx, key, incr).Result()
	if err == nil && result == 1 && expiration > 0 {
		client.Expire(ctx, key, expiration)
	}
	return result
}

func Get(ctx context.Context, key string) string {
	result, err := client.Get(ctx, key).Result()
	if err != nil && err != redis.Nil {
		global.Logger.Errorf(ctx, "redis.Get err:%v", err)
		return ""
	}
	return result
}

func Set(ctx context.Context, key string, value interface{}, expiration time.Duration) {
	_, err := client.Set(ctx, key, value, expiration).Result()
	if err != nil && err != redis.Nil {
		global.Logger.Errorf(ctx, "redis.Set err:%v", err)
	}
}

func DevSet(ctx context.Context, key string, value interface{}, expiration time.Duration) {
	_, err := client.Set(ctx, key, value, expiration).Result()
	if err != nil && err != redis.Nil {
		global.Logger.Errorf(ctx, "redis.Set err:%v", err)
	}
}

func GetSet(ctx context.Context, key string, value interface{}) (string, error) {
	result, err := client.GetSet(ctx, key, value).Result()
	if err != nil && err != redis.Nil {
		global.Logger.Errorf(ctx, "redis.GetSet err:%v", err)
		return "", err
	}
	return result, nil
}

func Del(key string) bool {
	ctx := context.Background()
	result, err := client.Del(ctx, key).Result()
	if err != nil {
		global.Logger.Errorf(ctx, "redis.Del err:%v", err)
		return false
	}
	return result > 0
}
